package com.xrui.hbaseview.ui.forms;

import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.xrui.hbaseview.actions.RunnableAction;
import com.xrui.hbaseview.config.ConnectionDetails;
import com.xrui.hbaseview.config.GlobalConfig;
import com.xrui.hbaseview.config.PropertiesConfig;
import com.xrui.hbaseview.config.ViewConfig;
import com.xrui.hbaseview.data.DataTable;

import com.xrui.hbaseview.hbase.Connection;
import com.xrui.hbaseview.hbase.ConnectionManager;
import com.xrui.hbaseview.io.Downloader;
import com.xrui.hbaseview.system.Clipboard;
import com.xrui.hbaseview.system.ClipboardData;
import com.xrui.hbaseview.system.InMemoryClipboard;
import com.xrui.hbaseview.system.Version;
import com.xrui.hbaseview.ui.*;
import com.xrui.hbaseview.ui.controls.JLinkButton;
import com.xrui.hbaseview.ui.controls.JTab;
import com.xrui.hbaseview.ui.views.DesignerView;
import com.xrui.hbase.util.ResourceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class Window {

    //region Constants
    private final static Logger LOG = LoggerFactory.getLogger(Window.class);
    private static final String BAMS_REPO_URL = "https://bams-sami.int.thomsonreuters.com" +
        "/artifactory/default.generic.global/";
    private static final String UPDATE_FILE_URL =
        BAMS_REPO_URL + "timeseries/ets/intraday/common/hbase-tools/update.properties";
    private static final String DOWNLOAD_URL =
        BAMS_REPO_URL + "timeseries/ets/intraday/common/hbase-tools";
    //endregion

    //region Variables
    private JPanel topPanel;
    private JTabbedPane tabbedPane;
    private JLinkButton connectToCluster;
    private JPanel actionPanel;
    private JLabel actionLabel1;
    private JLinkButton actionLabel2;
    private JLabel actionLabel3;
    private JButton copyToClipboard;
    private JLinkButton newVersionAvailable;
    private boolean canceled;
    private UIAction uiAction;
    private String lastError;
    private Map<String, List<DesignerView>> viewList;
    private Properties updateInfo;

    //endregion

    //region Private Methods

    //region Constructor
    public Window() {
        this.updateInfo = new Properties();
        this.viewList = new HashMap<>();

        Font font = this.actionLabel1.getFont();
        this.actionLabel1.setFont(font.deriveFont(font.getStyle() | ~Font.BOLD));
        this.actionLabel2.setFont(font.deriveFont(font.getStyle() | ~Font.BOLD));
        this.actionLabel3.setFont(font.deriveFont(font.getStyle() | ~Font.BOLD));

        MessageHandler.addListener(
            new MessageHandlerListener() {
                @Override
                public void onInfo(String message) {
                    lastError = null;
                    uiAction = null;

                    copyToClipboard.setVisible(false);

                    actionLabel1.setText(message);

                    // this trick allows to keep all labels on the same level. The empty text on the link button moves all labels up for a couple of pixels.
                    actionLabel2.setText(" ");
                    actionLabel2.setLinkColor(new Color(240, 240, 240)); // the control color which is not visible.

                    actionLabel3.setText("");
                }

                @Override
                public void onError(String message, Exception ex) {
                    uiAction = null;

                    copyToClipboard.setVisible(true);

                    String error = ' ' + message;
                    lastError = error;

                    if (ex != null) {
                        StringWriter sw = new StringWriter();
                        PrintWriter pw = new PrintWriter(sw);

                        ex.printStackTrace(pw);

                        lastError += ": " + sw.toString();
                        error += ": " + ex.getMessage();

                        pw.close();
                    }

                    actionLabel1.setText(error);
                    actionLabel2.setText(" ");
                    actionLabel2.setLinkColor(new Color(240, 240, 240));
                    actionLabel3.setText("");
                }

                @Override
                public void onAction(UIAction action) {
                    lastError = null;
                    uiAction = action;

                    copyToClipboard.setVisible(false);

                    String[] messageParts = action.getFormattedMessage();
                    if (messageParts.length > 0) {
                        actionLabel1.setText(messageParts[0]);
                    } else {
                        actionLabel1.setText("");
                    }

                    if (messageParts.length > 1) {
                        actionLabel2.setText(messageParts[1]);
                        actionLabel2.setLinkColor(new Color(0, 0, 255)); // the blue link color.
                    } else {
                        actionLabel2.setText(" ");
                        actionLabel2.setLinkColor(new Color(240, 240, 240));
                    }

                    if (messageParts.length > 2) {
                        actionLabel3.setText(messageParts[2]);
                    } else {
                        actionLabel3.setText("");
                    }
                }
            });

        this.connectToCluster.addActionListener(
            e -> {
                ConnectionDetails connectionDetails = showDialog();
                if (connectionDetails != null) {
                    try {
                        Connection connection = ConnectionManager.get().create(connectionDetails);
                        loadView(tabbedPane.getTabCount(), connection);
                    } catch (IOException ex) {
                        MessageHandler.addError(String.format("Failed to connect to %s.", connectionDetails.getZookeeper().getHost()), ex);
                    }
                }
            });

        this.actionLabel2.addActionListener(
            e -> {
                if (uiAction != null) {
                    uiAction.execute();
                }
            });

        this.copyToClipboard.addActionListener(
            e -> {
                if (lastError != null) {
                    Clipboard.setText(lastError);
                }
            });

        this.newVersionAvailable.addActionListener(
            e -> {
                UpdateDialog dialog = new UpdateDialog(
                    getVersion(),
                    updateInfo.getProperty("hbaseview.version"),
                    DOWNLOAD_URL,
                    updateInfo.getProperty("hbaseview.changes")
                    );
                dialog.showDialog(topPanel);
            });

        if (GlobalConfig.instance().getVersionCheckEnabled()) {
            RunnableAction.run(
                "compare-versions", new com.xrui.hbaseview.actions.Action<Object>() {
                    @Override
                    public Object run() throws Exception {
                        compareVersions();
                        return null;
                    }
                });
        }
    }

    //region Public Methods
    public static void main(String[] args) {
        // Schedule a job for the event-dispatching thread:
        // creating and showing this application's GUI.
        LOG.info("Starting HViewer...");
        SwingUtilities.invokeLater(Window::createAndShowGUI);
    }

    //endregion

    /**
     * Create the GUI and show it.  For thread safety,
     * this method should be invoked from the
     * event-dispatching thread.
     */
    private static void createAndShowGUI() {
        LOG.debug("Creating HViewer GUI...");
        final Window window = new Window();
        window.loadViews(new Splash());

        if (!window.canceled) {
            String version = getVersion();
            String title = "HViewer";
            if (version != null) {
                LOG.debug("HViewer version: {}", version);
                title += " - " + version;
            } else {
                LOG.debug("HViewer version unspecified");
            }
            JFrame frame = new JFrame(title);

            frame.addWindowListener(
                new WindowAdapter() {
                    @Override
                    public void windowClosing(WindowEvent e) {
                        LOG.info("Closing HViewer...");
                        window.saveViews();
                        LOG.info("HViewer closed");
                    }
                });

            frame.setContentPane(window.topPanel);
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.setLocationByPlatform(true);
            frame.setIconImage(Images.get("hviewer").getImage());

            // Display the window.
            frame.pack();
            frame.setVisible(true);
            LOG.info("HViewer started");
        } else {
            ResourceUtils.closeOrLog(ConnectionManager.get());
            System.exit(0);
        }
    }

    private static String getVersion() {
        try {
            return Window.class.getPackage().getImplementationVersion();
        } catch (Exception e) {
            LOG.warn("Failed to get version", e);
            return "";
        }
    }

    private void compareVersions() {
        FileInputStream stream = null;

        try {
            File updateFile = Downloader.download(new URL(UPDATE_FILE_URL));
            stream = new FileInputStream(updateFile);

            updateInfo.clear();
            updateInfo.load(stream);

            String newVersion = updateInfo.getProperty("hviewer.version");
            String oldVersion = getVersion();

            if (newVersion != null && oldVersion != null) {
                LOG.debug("Find latest version {} from {}", newVersion, UPDATE_FILE_URL);
                if (oldVersion.contains("-SNAPSHOT")) {
                    oldVersion = oldVersion.replace("-SNAPSHOT", "");
                }
                if (Version.compare(newVersion, oldVersion) > 0) {
                    LOG.debug("New version {} available", newVersion);
                    newVersionAvailable.setVisible(true);
                }
            }
        } catch (Exception e) {
            LOG.error(String.format("Failed to retrieve update information from the URL: %s", UPDATE_FILE_URL), e);
        } finally {
            ResourceUtils.closeOrLog(stream);
        }
    }

    private ConnectionDetails getConnectionDetails() {
        ConnectionDetails connectionDetails;
        Map<String, String> clusters = GlobalConfig.instance().getClusterConfigurations();
        if (clusters.size() == 1) {
            String path = clusters.get(clusters.keySet().iterator().next());
            connectionDetails = new ConnectionDetails(new File(path).getName());
        } else if (clusters.size() > 1) {
            connectionDetails = showDialog();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ignore) {
            }
        } else {
            connectionDetails = new ConnectionDetails();
        }
        return connectionDetails;
    }

    /**
     * Load all cluster views.
     */
    private void loadViews(final Splash splash) {
        LOG.info("Loading views...");
        ConnectionDetails connectionDetails = getConnectionDetails();

        if (connectionDetails != null) {
            new Thread(
                () -> {
                    try {
                        splash.update(String.format("Connecting to %s...", connectionDetails.getZookeeper().getHost()));
                        if (connectionDetails.canConnect()) {
                            splash.update(
                                String.format(
                                    "Loading view of %s...",
                                    connectionDetails.getZookeeper().getHost())
                            );

                            Connection connection = ConnectionManager.get().create(connectionDetails);
                            loadView(tabbedPane.getTabCount(), connection);
                        } else {
                            LOG.error("Connection to {} unavailable", connectionDetails.getZookeeper().getHost());
                            this.canceled = true;
                        }
                    } catch (IOException e) {
                        MessageHandler.addError(
                            String.format(
                                "Failed to connect to %s.",
                                connectionDetails.getZookeeper().getHost())
                            , e
                        );
                    }

                    splash.dispose();
                }).start();

            splash.showDialog();
        } else {
            this.canceled = true;
        }
    }

    private void saveViews() {
        for (List<DesignerView> views : viewList.values()) {
            for (DesignerView view : views) {
                view.saveView();
            }
        }
    }

    /**
     * Loads a single cluster view.
     *
     * @param connection A connection to be used to connect to the cluster.
     */
    private DesignerView loadView(int index, final Connection connection) {
        DesignerView view = new DesignerView(this.topPanel, connection);

        List<DesignerView> views = viewList.computeIfAbsent(connection.getClusterName(), k -> new ArrayList<>());

        views.add(view);

        JTab tab = new JTab(connection.getClusterName(), view, this.tabbedPane);
        tab.addTabActionListener(
            new TabActionListener() {
                @Override
                public void onTabClosed(DesignerView closingView) {
                    LOG.info("Closing view for {}...", connection.getClusterName());
                    List<DesignerView> list = viewList.get(closingView.getConnection().getClusterName());
                    list.remove(closingView);

                    ClipboardData<DataTable> data = InMemoryClipboard.getData();
                    if (data != null) {
                        Connection helper = data.getData().getConnection();
                        if (helper != null) {
                            if (helper.equals(closingView.getConnection())) {
                                InMemoryClipboard.setData(null);
                            }
                        }
                    }

                    if (list.isEmpty()) {
                        ViewConfig.instance().removeCluster(closingView.getConnection().getServerName());
                        PropertiesConfig.fileRemove(closingView.getConnection().getServerName());

                        viewList.remove(closingView.getConnection().getClusterName());
                    }

                    ConnectionManager.get().release(closingView.getConnection().getConnectionDetails());
                    LOG.info("View for {} closed", connection.getClusterName());
                }

                @Override
                public void onTabDuplicated(DesignerView sourceView) {
                    try {
                        LOG.info("Duplicating view for {}...", connection.getClusterName());
                        int newIndex = tabbedPane.indexOfComponent(sourceView.getView());

                        DesignerView duplicatedView = loadView(newIndex + 1, new Connection(connection.getConnectionDetails()));
                        duplicatedView.setSelectedTableName(sourceView.getSelectedTableName());
                        LOG.info("View for {} duplicated", connection.getClusterName());
                    } catch (IOException ex) {
                        MessageHandler.addError(String.format("Failed to duplicate view for %s.", connection.getClusterName()), ex);
                    }
                }
            });

        tabbedPane.insertTab(connection.getClusterName(), null, view.getView(), null, index);
        tabbedPane.setSelectedIndex(index);
        tabbedPane.setTabComponentAt(index, tab);

        if (views.size() == 1) {
            ViewConfig.instance().addCluster(connection.getServerName());
        }

        view.populate();
        return view;
    }

    /**
     * Shows a connection dialog.
     *
     * @return A reference to {@link ConnectionDetails} class that contains all required information to connect to the cluster.
     */
    private ConnectionDetails showDialog() {
        ConnectionDetailsDialog dialog = new ConnectionDetailsDialog();
        dialog.showDialog(this.topPanel);

        return dialog.getConnectionDetails();
    }

    {
// GUI initializer generated by IntelliJ IDEA GUI Designer
// >>> IMPORTANT!! <<<
// DO NOT EDIT OR ADD ANY CODE HERE!
        $$$setupUI$$$();
    }

    /**
     * Method generated by IntelliJ IDEA GUI Designer
     * >>> IMPORTANT!! <<<
     * DO NOT edit this method OR call it in your code!
     *
     * @noinspection ALL
     */
    private void $$$setupUI$$$() {
        topPanel = new JPanel();
        topPanel.setLayout(new GridLayoutManager(3, 1, new Insets(4, 4, 4, 4), -1, -1));
        topPanel.setMinimumSize(new Dimension(650, 450));
        topPanel.setPreferredSize(new Dimension(1200, 550));
        final JPanel panel1 = new JPanel();
        panel1.setLayout(new GridLayoutManager(1, 4, new Insets(0, 10, 0, 0), -1, -1));
        panel1.setBackground(new Color(-1));
        topPanel.add(panel1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_NORTH, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(-1, 50), null, 0, false));
        panel1.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), null));
        final JLabel label1 = new JLabel();
        label1.setIcon(new ImageIcon(getClass().getResource("/images/dbs_hadoop.png")));
        label1.setText("");
        panel1.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
        final JLabel label2 = new JLabel();
        label2.setFont(new Font("Jokerman", Font.BOLD, 29));
        label2.setForeground(new Color(-13408513));
        label2.setText(" HViewer");
        panel1.add(label2, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_VERTICAL, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
        final JPanel panel2 = new JPanel();
        panel2.setLayout(new FlowLayout(FlowLayout.RIGHT, 5, 5));
        panel2.setBackground(new Color(-1));
        panel1.add(panel2, new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
        panel2.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(), null));
        connectToCluster = new JLinkButton();
        connectToCluster.setFocusPainted(false);
        connectToCluster.setFocusable(false);
        connectToCluster.setHorizontalAlignment(0);
        connectToCluster.setRequestFocusEnabled(false);
        connectToCluster.setText("Connect to a cluster...");
        connectToCluster.setVisible(false);
        panel2.add(connectToCluster);
        newVersionAvailable = new JLinkButton();
        newVersionAvailable.setFocusPainted(false);
        newVersionAvailable.setFocusable(false);
        newVersionAvailable.setHorizontalAlignment(4);
        newVersionAvailable.setRequestFocusEnabled(false);
        newVersionAvailable.setText("New version is available...");
        newVersionAvailable.setVisible(false);
        panel2.add(newVersionAvailable);
        final JLabel label3 = new JLabel();
        label3.setFont(new Font("Vijaya", label3.getFont().getStyle(), 18));
        label3.setHorizontalAlignment(10);
        label3.setHorizontalTextPosition(11);
        label3.setText("  The generic HBase table viewer...");
        panel1.add(label3, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_VERTICAL, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
        final JPanel panel3 = new JPanel();
        panel3.setLayout(new BorderLayout(0, 0));
        topPanel.add(panel3, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(566, 377), null, 0, false));
        tabbedPane = new JTabbedPane();
        tabbedPane.setTabLayoutPolicy(1);
        panel3.add(tabbedPane, BorderLayout.CENTER);
        actionPanel = new JPanel();
        actionPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0));
        topPanel.add(actionPanel, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_SOUTH, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, new Dimension(-1, 34), new Dimension(-1, 34), null, 0, false));
        actionPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), null));
        copyToClipboard = new JButton();
        copyToClipboard.setIcon(new ImageIcon(getClass().getResource("/images/copy.png")));
        copyToClipboard.setLabel("");
        copyToClipboard.setMargin(new Insets(0, 0, 0, 0));
        copyToClipboard.setText("");
        actionPanel.add(copyToClipboard);
        actionLabel1 = new JLabel();
        actionLabel1.setFocusable(false);
        actionLabel1.setText("");
        actionPanel.add(actionLabel1);
        actionLabel2 = new JLinkButton();
        actionLabel2.setFocusPainted(false);
        actionLabel2.setFocusable(false);
        actionLabel2.setMargin(new Insets(0, 0, 0, 0));
        actionPanel.add(actionLabel2);
        actionLabel3 = new JLabel();
        actionLabel3.setFocusable(false);
        actionLabel3.setText("");
        actionPanel.add(actionLabel3);
    }

    /**
     * @noinspection ALL
     */
    public JComponent $$$getRootComponent$$$() {
        return topPanel;
    }
}
